﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Management;
using System.Text;
using System.Threading;
using Fantacid303.ProcessAffinityControl.Common;
using Fantacid303.ProcessAffinityControl.Config;

namespace Fantacid303.ProcessAffinityControl.Service
{
	public class ProcessManager
	{
		private Thread thread = null;
		private Timer timer;
		private bool shutdown = false;
		private bool configChanged = false;
		private readonly AutoResetEvent _event = new AutoResetEvent(false);
		private readonly Semaphore semaphore = new Semaphore(1, 1);

		private Dictionary<string, int> accessDeniedProcesses = new Dictionary<string, int>();
		private Logger logger;
		private ConfigurationManager configManager;
		private Fantacid303.ProcessAffinityControl.Config.Configuration actualConfig;
		private Fantacid303.ProcessAffinityControl.Config.Configuration newConfig;

		public ProcessManager()
		{
			this.logger = new Logger();
		}

		~ProcessManager()
		{
			if (this.logger != null)
				this.logger.Dispose();
		}

		public void Start()
		{
			if (this.thread != null)
				throw new ApplicationException("Already running!");

			this.thread = new Thread(this.ThreadMain);
			this.thread.Start();

			this.configManager = new ConfigurationManager();
			this.configManager.ConfigurationChanged += new EventHandler<ConfigurationChangedEventArgs>(ConfigManager_ConfigurationChanged);
			this.configManager.ReadConfig();
			this.actualConfig = this.configManager.Configuration;

			this.timer = new Timer(state => this._event.Set(), null, 0, this.actualConfig.Interval * 1000);
		}

		void ConfigManager_ConfigurationChanged(object sender, ConfigurationChangedEventArgs e)
		{
			this.newConfig = e.Configuration;
			this.configChanged = true;
			this._event.Set();
		}

		public void Shutdown()
		{
			this.Shutdown(true);
		}

		public void Shutdown(bool wait)
		{
			if (thread != null)
			{
				this.shutdown = true;
				this._event.Set();
				if (wait)
					this.thread.Join();
			}
		}

		private void ThreadMain()
		{
			try
			{
				this.Main();
			}
			finally
			{
				this.logger.Dispose();
				this.logger = null;
			}
		}


		private void SetAffinityMasks()
		{
			ConfigurationEntry activeConfigEntry = null;

			if (this.actualConfig != null)
				activeConfigEntry = this.actualConfig.ActiveEntry;


			foreach (var process in Process.GetProcesses())
			{
				try
				{
					ulong affinity = 0;
					var setAffinity = false;

					// if the process has not caused errors in the past
					if (this.IsErrorProcess(process.ProcessName))
						continue;
					if (this.actualConfig == null)
					{
						// set affinity. If affinity keeps zero, affinity will be reset (to all cores)
						setAffinity = true;
					}
					else
					{
						if (!this.actualConfig.ProcessIgnoreList.Exists(a => string.Compare(a, process.ProcessName, true) == 0))
							setAffinity = true;

						if (activeConfigEntry != null && setAffinity)
						{
							// check for non global process names (non *)
							var configMatch = activeConfigEntry.Rules.FirstOrDefault(a => string.Compare(a.ProcessName, process.ProcessName, true) == 0);
							if (configMatch != null)
							{
								affinity = ProcessorAffinityHelper.VirtualAffinityMaskToReal(configMatch.ProcessorAffinityType, configMatch.AffinityMask);
							}
							else
							{
								configMatch = activeConfigEntry.Rules.FirstOrDefault(a => a.ProcessName == "*");
								if (configMatch != null)
									affinity = ProcessorAffinityHelper.VirtualAffinityMaskToReal(configMatch.ProcessorAffinityType, configMatch.AffinityMask);
							}
						}
					}

					if (setAffinity)
					{
						var affinityToSet = (IntPtr)affinity;
						if (process.ProcessorAffinity != affinityToSet)
						{
							process.ProcessorAffinity = affinityToSet;
							this.logger.Log("Success", string.Format("Set affinity of process '{0}' to {1}", process.ProcessName, affinityToSet));
						}
					}
				}
				catch (Win32Exception w32Ex)
				{
					this.logger.Log("Error  ", string.Format("Cannot set affinity of process '{0}' : {1}", process.ProcessName, w32Ex.Message));

					if (w32Ex.NativeErrorCode == 5) // access denied
						this.MarkFailedProcess(process.ProcessName);

				}
				catch (System.Exception ex)
				{
					this.logger.Log("Error  ", string.Format("Cannot set affinity of process '{0}' : {1}", process.ProcessName, ex.Message));
				}

				// make sure, that the stuff gets written to the file at once
				this.logger.Flush();
			}

		}

		private bool IsErrorProcess(string processName)
		{
			var count = 0;
			if (this.accessDeniedProcesses.TryGetValue(processName, out count))
				return count > 2;
			return false;
		}

		private void MarkFailedProcess(string processName)
		{
			int count;

			if (this.accessDeniedProcesses.TryGetValue(processName, out count))
				count++;
			else
				count = 1;

			if (count>2)
				this.logger.Log("Info   ", string.Format("Process '{0}' will be ignored from now on.", processName));


			this.accessDeniedProcesses[processName] = count;
		}




		private void Main()
		{
			while (true)
			{

				this._event.WaitOne();
				if (this.shutdown)
				{
					this.timer.Dispose();
					return;
				}

				if (this.configChanged)
				{
					this.configChanged = false;
					if (this.newConfig != null)
					{
						if (this.actualConfig.Interval != this.newConfig.Interval)
							this.timer.Change(0, this.newConfig.Interval * 1000);

						this.actualConfig = this.newConfig;
						this.newConfig = null;
					}

				}

				if (this.semaphore.WaitOne(0))
				{
					try
					{
						this.SetAffinityMasks();
					}
					finally
					{
						this.semaphore.Release();
					}
				}
				else
				{
					this.logger.Log("Info   ", "Unable to enter semaphore");
				}

				this.logger.Flush();

			}
		}
	}
}
